﻿// <copyright file="Newswire.cs" company="Microsoft Corporation">
// Copyright (c) 2009 Microsoft Corporation All Rights Reserved
// </copyright>
// <author>Michael S. Scherotter</author>
// <email>mischero@microsoft.com</email>
// <date>2009-4-12</date>
// <summary>Class definition for New York Times Newswire CLR class for Silverlight</summary>

namespace NYTimes
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.Linq;
    using System.Net;
    using System.Text;
#if SILVERLIGHT
#else
    using System.Web;
#endif
    using System.Windows;
#if SILVERLIGHT && !WINDOWS_PHONE
    using System.Windows.Browser;
#endif
    using System.Windows.Media.Imaging;
    using System.Xml.Linq;
    using NYTimes.Core;
    using NYTimes.Data;
    using Synergist;

    /// <summary>
    /// Newswire sources
    /// </summary>
    public enum NewswireSource
    {
        /// <summary>
        /// Items from both The New York Times and The International Herald Tribune
        /// </summary>
        All,

        /// <summary>
        /// New York Times items only
        /// </summary>
        NYT,
        
        /// <summary>
        /// International Herald Tribune only
        /// </summary>
        IHT
    }

    /// <summary>
    /// New York Times Newswire API
    /// </summary>
    /// <remarks>See <a href="http://developer.nytimes.com/docs/times_newswire_api">
    /// this article</a> for more information.</remarks>
    public class Newswire : INotifyPropertyChanged
    {
        #region Fields
        /// <summary>
        /// the NYT Newswire Key in the App resources
        /// </summary>
        private const string NYTNewswireKey = "NYTNewswireKey";

        /// <summary>
        /// The NYT Newswire API Version
        /// </summary>
        private const string Version = "v3";

        /// <summary>
        /// 8 QPS rate limiting
        /// </summary>
        private static RateLimiting limiting = new RateLimiting("NYTNewswire", 8);

        /// <summary>
        /// are the results loading ansyn
        /// </summary>
        private bool isLoading;

        /// <summary>
        /// the current web client
        /// </summary>
        private WebClient client;

        /// <summary>
        /// the current offset
        /// </summary>
        private int offset;

        /// <summary>
        /// the URI to lookup 
        /// </summary>
        private Uri url;

        /// <summary>
        /// the response copyright
        /// </summary>
        private string copyright;

        /// <summary>
        /// the response status 
        /// </summary>
        private string status;

        /// <summary>
        /// New York Times Newswire API Key
        /// </summary>
        private string apiKey;

        /// <summary>
        /// The number of results to get
        /// </summary>
        private int resultsToGet;

        /// <summary>
        /// the newswire source
        /// </summary>
        private NewswireSource source = NewswireSource.All;

        /// <summary>
        /// the news item section
        /// </summary>
        private string section = "all";

        /// <summary>
        /// the time period in hours
        /// </summary>
        private int timePeriod;
        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the Newswire class
        /// </summary>
        public Newswire()
        {
            this.Results = new ObservableCollection<NewsItem>();

            this.ResultsToGet = 20;

            if (Application.Current.Resources.Contains(NYTNewswireKey))
            {
                this.ApiKey = Application.Current.Resources[NYTNewswireKey] as string;
            }

            if (SampleData.IsInDesignTool)
            {
                var xml = SampleData.GetSampleData("newswire.xml");
                
                if (!string.IsNullOrEmpty(xml))
                {
                    this.ParseXml(xml);
                }
            }
        }

        #endregion

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Property changed event handler
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the source (New York Times or International Herald Tribune)
        /// </summary>
        [Description("the source (New York Times or International Herald Tribune)")]
        public NewswireSource Source
        {
            get
            {
                return this.source;
            }

            set
            {
                if (this.source != value)
                {
                    this.source = value;

                    this.Offset = 0;
                }
            }
        }

        /// <summary>
        /// Gets or sets the section or sections to include (one or more section names separated by semicolons)
        /// </summary>
        [Description("the section or sections to include (one or more section names separated by semicolons)")]
        public string Section 
        {
            get
            {
                return this.section;
            }

            set
            {
                if (this.section != value)
                {
                    this.section = value;

                    this.Offset = 0;
                }
            }
        }

        /// <summary>
        /// Gets or sets the API Key
        /// </summary>
        [Description("The New York Times Newswire API Key")]
        public string ApiKey
        {
            get
            {
                return this.apiKey;
            }

            set
            {
                if (this.apiKey != value)
                {
                    this.apiKey = value;

                    this.Offset = 0;
                }
            }
        }

        /// <summary>
        /// Gets the response copyright message
        /// </summary>
        public string Copyright
        {
            get
            {
                return this.copyright;
            }

            private set
            {
                this.copyright = value;

                if (this.PropertyChanged != null)
                {
                    this.PropertyChanged(this, new PropertyChangedEventArgs("Copyright"));
                }
            }
        }

        /// <summary>
        /// Gets the response status
        /// </summary>
        public string Status
        {
            get
            {
                return this.status;
            }

            private set
            {
                this.status = value;

                if (this.PropertyChanged != null)
                {
                    this.PropertyChanged(this, new PropertyChangedEventArgs("Status"));
                }
            }
        }

        /// <summary>
        /// Gets a value indicating whether the results are being loaded asynchronously
        /// </summary>
        public bool IsLoading 
        { 
            get
            {
                return this.isLoading;
            }

            private set
            {
                if (this.isLoading != value)
                {
                    this.isLoading = value;

                    if (this.PropertyChanged != null)
                    {
                        this.PropertyChanged(this, new PropertyChangedEventArgs("IsLoading"));
                    }
                }
            }
        }

        /// <summary>
        /// Gets or sets the URI of a specific news item to retrieve.
        /// </summary>
        [Description("Gets or sets the URI of a specific news item to retrieve."),
         TypeConverter(typeof(UriTypeConverter))]
        public Uri Url
        { 
            get
            {
                return this.url;
            }

            set
            {
                this.url = value;

                this.Offset = 0;
            }
        }

        /// <summary>
        /// Gets or sets the total number of results to get (default is 20)
        /// </summary>
        [Description("The total number of results to get.  Default is 20.")]
        public int ResultsToGet
        {
            get
            {
                return this.resultsToGet;
            }

            set
            {
                if (this.resultsToGet != value)
                {
                    this.resultsToGet = value;

                    this.Offset = 0;
                }
            }
        }

        /// <summary>
        /// Gets or sets the time period in hours (1-720)
        /// </summary>
        [Description("The time period to get the articles from.")]
        public int TimePeriod
        {
            get
            {
                return this.timePeriod;
            }

            set
            {
                if (value < 1 || value > 720)
                {
                    throw new ArgumentOutOfRangeException("value");
                }

                if (this.timePeriod != value)
                {
                    this.timePeriod = value;

                    this.Offset = 0;
                }
            }
        }
        
        /// <summary>
        /// Gets the logo for New York Times
        /// </summary>
        [SuppressMessage("Microsoft.Performance", "CA1822", Justification = "This is exposed to a Silverlight application.")]
        public BitmapImage Logo
        {
            get
            {
                var streamResource = Application.GetResourceStream(
    new Uri("NYTimesSilverlightKit;component/Images/Logo.png", UriKind.Relative));

                var image = new BitmapImage();

#if SILVERLIGHT
                image.SetSource(streamResource.Stream);
#else
                image.StreamSource = streamResource.Stream;
#endif

                return image;
            }
        }

        /// <summary>
        /// Gets the Newswire results as an observable collection of <see cref="NYTimes.Data.NewsItem"/> objects.
        /// </summary>
        public ObservableCollection<NewsItem> Results { get; private set; }

        /// <summary>
        /// Gets the single <see cref="NYTimes.Data.NewsItem"/> object requested by a Url.
        /// </summary>
        public NewsItem Result { get; private set; }

        /// <summary>
        /// Gets or sets the offset for this call
        /// </summary>
        private int Offset
        {
            get
            {
                return this.offset;
            }

            set
            {
                this.offset = value;

                if (!SampleData.IsInDesignTool)
                {
                    if (this.offset == 0)
                    {
                        this.IsLoading = true;
                    }

                    this.FetchResults();
                }
            }
        } 

        #endregion

        #region Methods
        /// <summary>
        /// Refresh the result set
        /// </summary>
        public void Refresh()
        {
            this.Offset = 0;
        }
        #endregion

        #region Implementation

        /// <summary>
        /// Create a string collection from a semicolon-delimited field
        /// </summary>
        /// <param name="element">the element</param>
        /// <returns>an array of strings or null</returns>
        private static IEnumerable<string> CreateStringCollection(XElement element)
        {
            if (element == null)
            {
                return null;
            }
        
            if (string.IsNullOrEmpty(element.Value.Trim()))
            {
                return null;
            }

            return element.Value.Split(';');       
        }

        /// <summary>
        /// Fetch the results
        /// </summary>
        private void FetchResults()
        {
            if (this.client != null)
            {
                limiting.Cancel(this.client);

                this.client.CancelAsync();

                this.client = null;
            }

            var uri = this.GenerateUri();

            if (uri == null)
            {
                return;
            }

            System.Diagnostics.Debug.WriteLine(uri);

            this.client = new WebClient();

            this.client.DownloadStringCompleted += new DownloadStringCompletedEventHandler(this.OnResultsReturned);

            limiting.MakeWebRequest(this.client, uri);
        }

        /// <summary>
        /// Process the XML once it is returned
        /// </summary>
        /// <param name="sender">the web client</param>
        /// <param name="e">the download string completed event arguments</param>
        private void OnResultsReturned(object sender, DownloadStringCompletedEventArgs e)
        {
            this.client = null;

            if (e.Error != null)
            {
                if (!e.Cancelled)
                {
                    System.Diagnostics.Debug.WriteLine(e.Error.Message);

                    this.IsLoading = false;

                    this.Status = e.Error.Message;
                }

                return;
            }

            if (this.Offset == 0)
            {
                this.Results.Clear();
            }

            var xml = e.Result;

            this.ParseXml(xml);

            if (this.Results.Count < this.ResultsToGet && this.Url == null)
            {
                this.Offset = this.Results.Count;
            }
            else
            {
                this.IsLoading = false;
            }
        }

        /// <summary>
        /// Parse the XML returned from the service
        /// </summary>
        /// <param name="xml">the XML returned from the service</param>
        private void ParseXml(string xml)
        {
            var doc = XDocument.Parse(xml);

            var items = from newsItem in doc.Descendants("news_item")
                        select new Data.NewsItem()
                        {
                            BlogName = newsItem.Element("blog_name") == null ? null : newsItem.Element("blog_name").Value,
                            Byline = newsItem.Element("byline").Value,
                            Created = DateTime.Parse(newsItem.Element("created_date").Value, CultureInfo.InvariantCulture),
                            Headline = newsItem.Element("title").Value,
                            ////Id = long.Parse(newsItem.Element("id").Value, CultureInfo.InvariantCulture),
                            Kicker = newsItem.Element("kicker").Value,
                            Locations = CreateStringCollection(newsItem.Element("geo_facet")),
                            Media = from mediaItem in newsItem.Descendants("multimedia_item")
                                    select new NewsItemMedia
                                    {
                                        Caption = mediaItem.Element("caption").Value,
                                        Copyright = mediaItem.Element("copyright").Value,
                                        MediaType = mediaItem.Attribute("type").Value,
                                        Metadata = (from metadataItem in mediaItem.Descendants("media-metadata_item")
                                                    select new MediaMetadata
                                                    {
                                                        Format = metadataItem.Attribute("format").Value,
                                                        Height = int.Parse(metadataItem.Attribute("height").Value, CultureInfo.InvariantCulture),
                                                        Url = new Uri(metadataItem.Element("url").Value, UriKind.Absolute),
                                                        Width = int.Parse(metadataItem.Attribute("width").Value, CultureInfo.InvariantCulture)
                                                    }).First(),
                                        Subtype = mediaItem.Element("subtype").Value
                                    },
                            Organizations = from orgFacetItem in newsItem.Element("org_facet").Elements("org_facet_item")
                                            select orgFacetItem.Value,
                            People = from perFacet in newsItem.Element("per_facet").Elements("per_facet")
                                     select perFacet.Value,
                            Platform = newsItem.Element("platform") == null ? null : newsItem.Element("platform").Value,
                            PubDate = DateTime.Parse(newsItem.Element("published_date").Value, CultureInfo.InvariantCulture),
                            RelatedUrls = from link in newsItem.Element("related_urls").Elements("link")
                                          select new Link
                                          {
                                              SuggestedLinkText = link.Element("suggested_link_text").Value,
                                              Url = new Uri(link.Element("url").Value, UriKind.Absolute),
                                          },
                            Section = newsItem.Element("section").Value,
                            Source = newsItem.Element("source").Value,
                            SubHeadline = newsItem.Element("subheadline").Value,
                            ////Subsection = newsItem.Element("subsection") == null ? null : newsItem.Element("subsection").Value,
                            Subtype = newsItem.Element("material_type_facet").Value,
                            Summary = newsItem.Element("summary") == null ? null : Uri.UnescapeDataString(newsItem.Element("summary").Value),
                            Terms = from desFacetItem in newsItem.Element("des_facet").Elements("des_facet_item")
                                    select desFacetItem.Value,
                            ItemType = newsItem.Element("item_type").Value,
                            Updated = DateTime.Parse(newsItem.Element("updated_date").Value, CultureInfo.InvariantCulture),
                            Url = new Uri(newsItem.Element("url").Value, UriKind.Absolute),
                        };

            if (this.Url == null)
            {
                foreach (var item in items)
                {
                    this.Results.Add(item);
                }
            }
            else
            {
                this.Result = items.FirstOrDefault();
            }

            var resultSet = doc.Element("result_set");

            this.Copyright = resultSet.Element("copyright").Value;

            this.Status = resultSet.Element("status").Value;
        }

        /// <summary>
        /// Generate a URI
        /// </summary>
        /// <returns>the URI for the Newswire service call</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Not relevant for Silverlight"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Not relevant for Silverlight")]
        private Uri GenerateUri()
        {
            if (string.IsNullOrEmpty(this.ApiKey))
            {
                return null;
            }

            var urlString = string.Format(
                CultureInfo.InvariantCulture, 
                "http://api.nytimes.com/svc/news/{0}/content",
                Version);

            var builder = new StringBuilder(urlString);

            if (this.Url == null)
            {
                builder.AppendFormat(
                    CultureInfo.InvariantCulture,
                    "/{0}/{1}",
                    this.Source.ToString().ToLower(CultureInfo.InvariantCulture),
                    this.Section);

                if (this.TimePeriod > 0)
                {
                    builder.AppendFormat(CultureInfo.InvariantCulture, "/{0}", this.TimePeriod);
                }

                builder.AppendFormat(CultureInfo.InvariantCulture, ".xml?api-key={0}", this.ApiKey);

                var limit = Math.Min(this.ResultsToGet - this.Results.Count, 20);

                if (limit > 0)
                {
                    builder.AppendFormat(CultureInfo.InvariantCulture, "&limit={0}", limit);
                }

                if (this.Offset > 0)
                {
                    builder.AppendFormat(CultureInfo.InvariantCulture, "&offset={0}", this.Offset);
                }
            }
            else
            {
                //// var encodedUrl =  this.Url.ToString().Replace(@"/", @"\/");  

                var encodedUrl = Uri.EscapeUriString(this.Url.ToString());

                builder.AppendFormat(CultureInfo.InvariantCulture, ".xml?url={0}&api-key={1}", encodedUrl, this.ApiKey);
            }

            return new Uri(builder.ToString(), UriKind.Absolute);
        }

        #endregion
    }
}
